/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.dq.core.service.impl.function.java;

import java.io.ByteArrayInputStream;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.jar.JarInputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.unidata.mdm.core.dto.UserLibraryResult;
import org.unidata.mdm.dq.core.exception.DataQualityExceptionIds;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunction;
import org.unidata.mdm.dq.core.type.cleanse.CleanseFunctionLibrary;
import org.unidata.mdm.dq.core.type.model.instance.CleanseFunctionElement;
import org.unidata.mdm.dq.core.type.model.instance.JavaFunctionElement;
import org.unidata.mdm.system.exception.PlatformBusinessException;
import org.unidata.mdm.system.exception.PlatformFailureException;
import org.unidata.mdm.system.service.ModuleService;
import org.unidata.mdm.system.util.JarUtils.SingleJarClassLoader;

/**
 * @author Mikhail Mikhailov on Feb 3, 2021
 */
public class JavaCleanseFunctionLibrary extends CleanseFunctionLibrary {
    /**
     * This class logger.
     */
    private static final Logger LOGGER = LoggerFactory.getLogger(JavaCleanseFunctionLibrary.class);
    /**
     * Base library name.
     */
    public static final String DQ_SYSTEM_CLEANSE_FUNCTIONS_LIBRARY = "system-cleanse-functions.jar";
    /**
     * Base library version.
     */
    public static final String DQ_SYSTEM_CLEANSE_FUNCTIONS_VERSION = "1.0.0";
    /**
     * This class loader.
     */
    private final SingleJarClassLoader classLoader;
    /**
     * The MS instance
     */
    private final ModuleService moduleService;
    /**
     * Beans.
     */
    private final Map<String, CleanseFunction> beans = new ConcurrentHashMap<>();
    /**
     * Constructor.
     */
    public JavaCleanseFunctionLibrary(ModuleService moduleService, UserLibraryResult ulr) {
        super(ulr);
        this.moduleService = moduleService;
        try (JarInputStream jis = new JarInputStream(new ByteArrayInputStream(ulr.getPayload()))) {
            this.classLoader = new SingleJarClassLoader(this.name, Thread.currentThread().getContextClassLoader(), jis);
        } catch (Exception e) {
            throw new PlatformFailureException("Exception caught while creating Java library instance.",
                    e, DataQualityExceptionIds.EX_DQ_LIBRARY_CANNOT_BE_CREATED);
        }
    }
    /**
     * Gets function implementation
     * @param name the function name
     * @param className the class name
     * @return function instance or null, if not found
     */
    @Override
    public CleanseFunction lookup(CleanseFunctionElement el) {

        if (!el.isJavaFunction()) {
            throw new PlatformBusinessException("Cleanse function element [{}] is not a java element.",
                    DataQualityExceptionIds.EX_DQ_ELEMENT_IS_NOT_JAVA, el.getDisplayName());
        }

        JavaFunctionElement jfe = el.getJavaFunction();
        String functionName = jfe.getId();
        String className = jfe.getFunctionClassName();

        return lookup(functionName, className);
    }
    /**
     * Gets function implementation by function name and class FQN.
     * @param functionName the function name
     * @param className the class name
     * @return cleanse function
     */
    public CleanseFunction lookup(String functionName, String className) {
        return beans.computeIfAbsent(functionName, key -> wireFunction(key, className));
    }
    /**
     * Lazy creates function implementation.
     * @param functionName the function name
     * @param className the class name
     * @return function or NIL (not found/unreachable) indicator
     */
    private CleanseFunction wireFunction(String functionName, String className) {

        try {

            Class<?> klass = Class.forName(className, true, this.classLoader);

            if (!CleanseFunction.class.isAssignableFrom(klass)) {
                throw new IllegalArgumentException("The class "
                        + klass.getName()
                        + " is NOT an instance of CleanseFunction interface!");
            }

            Object o = klass.getConstructor().newInstance();
            return moduleService.initOrphanBean(functionName, (CleanseFunction) o);

        } catch (Exception e) {
            throw new PlatformFailureException("Cannot create Java cleanse function instance for [{}].", e,
                    DataQualityExceptionIds.EX_DQ_JAVA_FUNCTION_CANNOT_BE_CREATED,
                    functionName);
        }
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void cleanup() {

        beans.forEach((name, bean) -> {
            try {
                // Call @PreDestroy, destroy() etc.
                moduleService.releaseOrphanBean(bean);
            } catch (Exception e) {
                LOGGER.error("Exception caught while passivating a Java Cleanse Function [{}].", name, e);
            }
        });

        beans.clear();
        classLoader.cleanup();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public Collection<String> suggest() {
        return classLoader.filterAssignableNames(CleanseFunction.class);
    }
}
